// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:pigeon/src/ast.dart';
import 'package:pigeon/src/generator_tools.dart';
import 'package:test/test.dart';

bool _equalSet<T>(Set<T> x, Set<T> y) {
  if (x.length != y.length) {
    return false;
  }
  for (final object in x) {
    if (!y.contains(object)) {
      return false;
    }
  }
  return true;
}

bool _equalMaps(Map<String, Object> x, Map<String, Object> y) {
  if (!_equalSet(x.keys.toSet(), y.keys.toSet())) {
    return false;
  }
  for (final String key in x.keys) {
    final Object xValue = x[key]!;
    if (xValue is Map<String, Object>) {
      if (!_equalMaps(xValue, (y[key] as Map<String, Object>?)!)) {
        return false;
      }
    } else {
      if (xValue != y[key]) {
        return false;
      }
    }
  }
  return true;
}

final Class emptyClass = Class(
  name: 'className',
  fields: <NamedType>[
    NamedType(
      name: 'namedTypeName',
      type: const TypeDeclaration(baseName: 'baseName', isNullable: false),
    ),
  ],
);

void main() {
  test('test merge maps', () {
    final source = <String, Object>{
      '1': '1',
      '2': <String, Object>{'1': '1', '3': '3'},
      '3': '3', // not modified
    };
    final modification = <String, Object>{
      '1': '2', // modify
      '2': <String, Object>{
        '2': '2', // added
      },
    };
    final expected = <String, Object>{
      '1': '2',
      '2': <String, Object>{'1': '1', '2': '2', '3': '3'},
      '3': '3',
    };
    expect(_equalMaps(expected, mergeMaps(source, modification)), isTrue);
  });

  test('get codec types from all classes and enums', () {
    final root = Root(
      classes: <Class>[
        Class(
          name: 'name',
          fields: <NamedType>[
            NamedType(
              name: 'name',
              type: const TypeDeclaration(baseName: 'name', isNullable: true),
            ),
          ],
        ),
      ],
      apis: <Api>[],
      enums: <Enum>[
        Enum(
          name: 'enum',
          members: <EnumMember>[EnumMember(name: 'enumMember')],
        ),
      ],
    );
    final List<EnumeratedType> types = getEnumeratedTypes(root).toList();
    expect(types.length, 2);
  });

  test('getEnumeratedTypes:ed type arguments', () {
    final root = Root(
      apis: <Api>[
        AstFlutterApi(
          name: 'Api',
          methods: <Method>[
            Method(
              name: 'foo',
              location: ApiLocation.flutter,
              parameters: <Parameter>[
                Parameter(
                  name: 'x',
                  type: TypeDeclaration(
                    isNullable: false,
                    baseName: 'List',
                    typeArguments: <TypeDeclaration>[
                      TypeDeclaration(
                        baseName: 'Foo',
                        isNullable: true,
                        associatedClass: emptyClass,
                      ),
                    ],
                  ),
                ),
              ],
              returnType: const TypeDeclaration.voidDeclaration(),
            ),
          ],
        ),
      ],
      classes: <Class>[
        Class(
          name: 'Foo',
          fields: <NamedType>[
            NamedType(
              name: 'bar',
              type: TypeDeclaration(
                baseName: 'Bar',
                isNullable: true,
                associatedClass: emptyClass,
              ),
            ),
          ],
        ),
        Class(
          name: 'Bar',
          fields: <NamedType>[
            NamedType(
              name: 'value',
              type: const TypeDeclaration(baseName: 'int', isNullable: true),
            ),
          ],
        ),
      ],
      enums: <Enum>[],
    );
    final List<EnumeratedType> classes = getEnumeratedTypes(root).toList();
    expect(classes.length, 2);
    expect(
      classes.where((EnumeratedType element) => element.name == 'Foo').length,
      1,
    );
    expect(
      classes.where((EnumeratedType element) => element.name == 'Bar').length,
      1,
    );
  });

  test('getEnumeratedTypes: Object', () {
    final root = Root(
      apis: <Api>[
        AstFlutterApi(
          name: 'Api1',
          methods: <Method>[
            Method(
              name: 'foo',
              location: ApiLocation.flutter,
              parameters: <Parameter>[
                Parameter(
                  name: 'x',
                  type: const TypeDeclaration(
                    isNullable: false,
                    baseName: 'List',
                    typeArguments: <TypeDeclaration>[
                      TypeDeclaration(baseName: 'Object', isNullable: true),
                    ],
                  ),
                ),
              ],
              returnType: const TypeDeclaration.voidDeclaration(),
            ),
          ],
        ),
      ],
      classes: <Class>[
        Class(
          name: 'Foo',
          fields: <NamedType>[
            NamedType(
              name: 'bar',
              type: const TypeDeclaration(baseName: 'int', isNullable: true),
            ),
          ],
        ),
      ],
      enums: <Enum>[],
    );
    final List<EnumeratedType> classes = getEnumeratedTypes(root).toList();
    expect(classes.length, 1);
    expect(
      classes.where((EnumeratedType element) => element.name == 'Foo').length,
      1,
    );
  });

  test('getEnumeratedTypes:ue entries', () {
    final root = Root(
      apis: <Api>[
        AstFlutterApi(
          name: 'Api1',
          methods: <Method>[
            Method(
              name: 'foo',
              location: ApiLocation.flutter,
              parameters: <Parameter>[
                Parameter(
                  name: 'x',
                  type: TypeDeclaration(
                    isNullable: false,
                    baseName: 'Foo',
                    associatedClass: emptyClass,
                  ),
                ),
              ],
              returnType: const TypeDeclaration.voidDeclaration(),
            ),
          ],
        ),
        AstHostApi(
          name: 'Api2',
          methods: <Method>[
            Method(
              name: 'foo',
              location: ApiLocation.host,
              parameters: <Parameter>[
                Parameter(
                  name: 'x',
                  type: TypeDeclaration(
                    isNullable: false,
                    baseName: 'Foo',
                    associatedClass: emptyClass,
                  ),
                ),
              ],
              returnType: const TypeDeclaration.voidDeclaration(),
            ),
          ],
        ),
      ],
      classes: <Class>[
        Class(
          name: 'Foo',
          fields: <NamedType>[
            NamedType(
              name: 'bar',
              type: const TypeDeclaration(baseName: 'int', isNullable: true),
            ),
          ],
        ),
      ],
      enums: <Enum>[],
    );
    final List<EnumeratedType> classes = getEnumeratedTypes(root).toList();
    expect(classes.length, 1);
    expect(
      classes.where((EnumeratedType element) => element.name == 'Foo').length,
      1,
    );
  });

  test('deduces package name successfully', () {
    final String? dartPackageName = deducePackageName(
      './pigeons/core_tests.dart',
    );

    expect(dartPackageName, 'pigeon');
  });

  test('recursiveGetSuperClassApisChain', () {
    final superClassOfSuperClassApi = AstProxyApi(
      name: 'Api3',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
    );
    final superClassApi = AstProxyApi(
      name: 'Api2',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
      superClass: TypeDeclaration(
        baseName: 'Api3',
        isNullable: false,
        associatedProxyApi: superClassOfSuperClassApi,
      ),
    );
    final api = AstProxyApi(
      name: 'Api',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
      superClass: TypeDeclaration(
        baseName: 'Api2',
        isNullable: false,
        associatedProxyApi: superClassApi,
      ),
    );

    expect(
      api.allSuperClasses().toList(),
      containsAllInOrder(<AstProxyApi>[
        superClassApi,
        superClassOfSuperClassApi,
      ]),
    );
  });

  test('recursiveFindAllInterfacesApis', () {
    final interfaceOfInterfaceApi2 = AstProxyApi(
      name: 'Api5',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
    );
    final interfaceOfInterfaceApi = AstProxyApi(
      name: 'Api4',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
    );
    final interfaceApi2 = AstProxyApi(
      name: 'Api3',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
      interfaces: <TypeDeclaration>{
        TypeDeclaration(
          baseName: 'Api5',
          isNullable: false,
          associatedProxyApi: interfaceOfInterfaceApi2,
        ),
      },
    );
    final interfaceApi = AstProxyApi(
      name: 'Api2',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
      interfaces: <TypeDeclaration>{
        TypeDeclaration(
          baseName: 'Api4',
          isNullable: false,
          associatedProxyApi: interfaceOfInterfaceApi,
        ),
        TypeDeclaration(
          baseName: 'Api5',
          isNullable: false,
          associatedProxyApi: interfaceOfInterfaceApi2,
        ),
      },
    );
    final api = AstProxyApi(
      name: 'Api',
      methods: <Method>[],
      constructors: <Constructor>[],
      fields: <ApiField>[],
      interfaces: <TypeDeclaration>{
        TypeDeclaration(
          baseName: 'Api2',
          isNullable: false,
          associatedProxyApi: interfaceApi,
        ),
        TypeDeclaration(
          baseName: 'Api3',
          isNullable: false,
          associatedProxyApi: interfaceApi2,
        ),
      },
    );

    expect(
      api.apisOfInterfaces(),
      containsAll(<AstProxyApi>[
        interfaceApi,
        interfaceApi2,
        interfaceOfInterfaceApi,
        interfaceOfInterfaceApi2,
      ]),
    );
  });

  test(
    'recursiveFindAllInterfacesApis throws error if api recursively implements itself',
    () {
      final a = AstProxyApi(
        name: 'A',
        methods: <Method>[],
        constructors: <Constructor>[],
        fields: <ApiField>[],
      );
      final b = AstProxyApi(
        name: 'B',
        methods: <Method>[],
        constructors: <Constructor>[],
        fields: <ApiField>[],
      );
      final c = AstProxyApi(
        name: 'C',
        methods: <Method>[],
        constructors: <Constructor>[],
        fields: <ApiField>[],
      );

      a.interfaces = <TypeDeclaration>{
        TypeDeclaration(
          baseName: 'B',
          isNullable: false,
          associatedProxyApi: b,
        ),
      };
      b.interfaces = <TypeDeclaration>{
        TypeDeclaration(
          baseName: 'C',
          isNullable: false,
          associatedProxyApi: c,
        ),
      };
      c.interfaces = <TypeDeclaration>{
        TypeDeclaration(
          baseName: 'A',
          isNullable: false,
          associatedProxyApi: a,
        ),
      };

      expect(() => a.apisOfInterfaces(), throwsArgumentError);
    },
  );

  test('findHighestApiRequirement', () {
    final typeWithoutMinApi = TypeDeclaration(
      baseName: 'TypeWithoutMinApi',
      isNullable: false,
      associatedProxyApi: AstProxyApi(
        name: 'TypeWithoutMinApi',
        methods: <Method>[],
        constructors: <Constructor>[],
        fields: <ApiField>[],
      ),
    );

    final typeWithMinApi = TypeDeclaration(
      baseName: 'TypeWithMinApi',
      isNullable: false,
      associatedProxyApi: AstProxyApi(
        name: 'TypeWithMinApi',
        methods: <Method>[],
        constructors: <Constructor>[],
        fields: <ApiField>[],
      ),
    );

    final typeWithHighestMinApi = TypeDeclaration(
      baseName: 'TypeWithHighestMinApi',
      isNullable: false,
      associatedProxyApi: AstProxyApi(
        name: 'TypeWithHighestMinApi',
        methods: <Method>[],
        constructors: <Constructor>[],
        fields: <ApiField>[],
      ),
    );

    final ({TypeDeclaration type, int version})? result =
        findHighestApiRequirement(
          <TypeDeclaration>[
            typeWithoutMinApi,
            typeWithMinApi,
            typeWithHighestMinApi,
          ],
          onGetApiRequirement: (TypeDeclaration type) {
            if (type == typeWithMinApi) {
              return 1;
            } else if (type == typeWithHighestMinApi) {
              return 2;
            }

            return null;
          },
          onCompare: (int one, int two) => one.compareTo(two),
        );

    expect(result?.type, typeWithHighestMinApi);
    expect(result?.version, 2);
  });

  test('Indent.format trims indentation', () {
    final buffer = StringBuffer();
    final indent = Indent(buffer);

    indent.format('''
      void myMethod() {

        print('hello');
      }''');

    expect(buffer.toString(), '''
void myMethod() {

  print('hello');
}
''');
  });
}
